<template>
  <div>
    <h2>上传文件</h2>
    <div ref="drag" class="drag">
      <input class="file" type="file" @change="handlerChange" />
    </div>
    <el-progress style="width: 500px;" :percentage="progress"></el-progress>
    <div style="margin-top: 16px;">
      <el-button type="primary" @click="upload">上传</el-button>
    </div>
    <div>
      <p>hash进度条</p>
      <el-progress style="width: 500px;" :percentage="hashProgress"></el-progress>
    </div>
    <div>
      <p>网格进度条</p>
      <ul class="grid" :style="{'width': gridWidth + 'px'}">
        <li class="grid-block" v-for="chunk in chunks" :key="chunk.name">
          <div 
            :class="{ 'uploading': chunk.progress > 0 && chunk.progress < 100, 'success': chunk.progress == 100, 'error': chunk.progress < 0}"
            :style="{height: chunk.progress + '%'}"
          >
            <i class="el-icon-loading" style="color: #f56c6c" v-if="chunk.progress < 100 && chunk.progress > 0"></i>
          </div>
        </li>
      </ul>
    </div>
  </div>
</template>

<script>
import Spark from 'spark-md5'
const CHUNK_SIZE = 1 * 1024 * 1024 //每次分片大小

export default {
  data () {
    return {
      fileData: null,
      progress: 0,
      hashProgress: 0,
      chunks: []
    }
  },
  computed: {
    gridWidth () {
      return Math.ceil(Math.sqrt(this.chunks.length)) * 24
    },
    uploadProgress () {
      if (!this.chunks.length || !this.fileData) {
        return 0
      }
      const loaded = this.chunks.map(item => item.progress * item.chunk.size).reduce((prev, cur) => prev + cur)
      return Number(((loaded * 100) / this.fileData.size).toFixed(2))
    }
  },
  mounted () {
    this.getUserInfo()
    this.dragRelevant()
  },
  methods: {
    //上传请求
    async uploadRequest (hash) {
      //如果已经上传过了 就不用上传了  用filter过滤掉
      const requests = this.chunks.map((chunk, index) => {
        if (chunk.progress === 100) {
          return null
        } else {
          const form = new FormData()
          form.append('chunk', chunk.file)
          form.append('hash', chunk.hash)
          form.append('name', chunk.name)
          return { form, index, error: 0 }
        }
      }).filter(val => val)
      // await Promise.all(requests)
      //实现并发数控制
      await this.sendRequest(requests)
      //合并上传的分片
      this.mergeFile(hash)
      //单文件上传
      // const formData = new FormData()
      // formData.append('name', 'file')
      // formData.append('file', this.fileData)
      // await this.$axios.post('/uploadFile', formData, {
      //   onUploadProgress: data => {
      //     this.progress = ((data.loaded / data.total) * 100).toFixed(2) - 0
      //   }
      // })
    },
    //合并文件
    mergeFile (hash) {
      return this.$axios.post('/merge', {
        ext: this.fileData.name.split('.').pop(),
        size: CHUNK_SIZE,
        hash
      })
    },
    //抽离上传文件接口
    launchRequest (file) {
      return this.$axios.post('/uploadFile', file.form, {
        onUploadProgress: data => {
          const progress = ((data.loaded / data.total) * 100).toFixed(2) - 0
          this.$set(this.chunks[file.index], 'progress', progress)
        }
      })
    },
    //请求并发数控制
    sendRequest (requests, limit = 3) {
      return new Promise((resolve, reject) => {
        const len = requests.length
        let counter = 0
        let isStop = false //如果一个片段失败超过三次 认为当前网洛有问题 停止全部上传
        const startRequest = async () => {
          if (isStop) return
          const task = requests.shift()
          if (task) {
            //利用try...catch捕获错误
            try {
              //具体的接口  抽离出去了
              await this.launchRequest(task)
              if (counter === len - 1) { //最后一个任务
                resolve()
              } else { //否则接着执行
                counter++
                startRequest() //启动下一个任务
              }
            } catch (error) {
              this.$set(this.chunks[task.index], 'progress', -1)
              //接口报错重试，限制为3次
              if (task.error < 3) {
                task.error++
                requests.unshift(task)
                startRequest()
              } else {
                isStop = true
                reject(error)
              }
            }
          }
        }
        //启动任务
        while (limit > 0) {
          //模拟不同大小启动
          setTimeout(() => {
            startRequest()
          }, Math.random() * 2000)
          limit--
        }
      })
    },
    //拖拽事件
    dragRelevant () {
      const dragDom = this.$refs.drag
      dragDom.addEventListener('dragover', e => {
        dragDom.style.borderColor = '#f00'
        e.preventDefault()
      })
      dragDom.addEventListener('dragleave', e => {
        dragDom.style.borderColor = '#41B883'
        e.preventDefault()
      })
      dragDom.addEventListener('drop', e => {
        dragDom.style.borderColor = '#41B883'
        const [file] = e.dataTransfer.files
        if (!file) return
        this.fileData = file
        e.preventDefault()
      })
    },
    //将文件转为16进制
    blobToString (blob) {
      return new Promise((resolve, reject) => {
        const reader = new FileReader()
        reader.readAsBinaryString(blob)
        reader.onload = function () {
          const result = reader.result.split('')
            .map(val => val.charCodeAt())
            .map(val => val.toString(16).toUpperCase())
            .map(val => val.padStart(2, 0))
            .join(' ')
          resolve(result)
        }
      })
    },
    //gif图16进制头六位为'47 49 46 38 39 61' || '47 49 46 38 37 61'
    async isGif (file) {
      const result = await this.blobToString(file.slice(0, 6))
      const isGif = result === '47 49 46 38 39 61' || result === '47 49 46 38 37 61'
      return isGif
    },
    //png图头信息前8位是89 50 4E 47 0D 0A 1A 0A
    async isPng (file) {
      const result = await this.blobToString(file.slice(0, 8))
      const isPng = result === '89 50 4E 47 0D 0A 1A 0A'
      return isPng
    },
    //jpg 头信息  前两位是FF D8  后两位是FF D9
    async isJpg (file) {
      const len = file.size
      const head = await this.blobToString(file.slice(0, 2))
      const tail = await this.blobToString(file.slice(-2, len))
      const isJpg = head === 'FF D8' && tail === 'FF D9'
      return isJpg
    },
    async isImage (file) {
      return await this.isGif(file) || await this.isPng(file) || await this.isJpg(file)
    },
    //创建文件片段
    createChunk (file, size = CHUNK_SIZE) {
      const chunks = []
      let count = 0
      while (count < file.size) {
        chunks.push({
          index: count,
          file: file.slice(count, count + size)
        })
        count += size
      }
      return chunks
    },
    //使用webWorker来计算文件的md5值
    calculateHashByWebWorker (chunks) {
      this.hashProgress = 0
      return new Promise(resolve => {
        const worker = new Worker('/hash.js')
        worker.postMessage(chunks)
        worker.onmessage = e => {
          const { hash, progress } = e.data
          this.hashProgress = progress
          if (hash) {
            resolve(hash)
          }
        }
      })
    },
    //使用requestIdleCallbck来计算文件的md5值  这个方法会在浏览器空闲时调用
    calculateHashByRequestIdleCallback (chunks) {
      return new Promise(resolve => {
        const spark = new Spark.ArrayBuffer()
        let count = 0
        const appendToSpark = file => {
          return new Promise(resolve => {
            const reader = new FileReader()
            reader.readAsArrayBuffer(file)
            reader.onload = data => {
              spark.append(data.target.result)
              resolve()
            }
          })
        }
        const workLoop = async deadLine => {
          while (count < chunks.length && deadLine.timeRemaining() > 1) {
            await appendToSpark(chunks[count].file)
            count++
            if (count < chunks.length) {
              this.hashProgress = (count * 100 / chunks.length).toFixed(2) - 0
            } else {
              this.hashProgress = 100
              resolve(spark.end())
            }
          }
          window.requestIdleCallback(workLoop)
        }
        window.requestIdleCallback(workLoop)
      })
    },
    //抽样hash 取前两个和后一个  中间每兆取前中后三个点
    calulateSamplingHash (chunks) {
      return new Promise(resolve => {
        const spark = new Spark.ArrayBuffer()
        const head = chunks.slice(0, 2)
        const tail = chunks[chunks.length - 1]
        const middle = chunks.slice(2, chunks.length - 1)
        const files = []
        files.push(head[0].file, head[1].file)
        middle.forEach(item => {
          const head = item.file.slice(0, 1)
          const tail = item.file.slice(-1, item.file.length)
          const center = Math.floor(item.file.length - 1) / 2
          const middle = item.file.slice(center, center + 1)
          files.push(head, tail, middle)
        })
        files.push(tail.file)
        //追加计算hash
        const reader = new FileReader()
        reader.readAsArrayBuffer(new Blob(files))
        reader.onload = data => {
          spark.append(data.target.result)
          this.hashProgress = 100
          resolve(spark.end())
        }
      })
    },
    //将每个循环内部都抽成promise
    pullHander (spark, chunk, index) {
      return new Promise((resolve, reject) => {
        chunk.name = 'file' + index
        const reader = new FileReader()
        reader.readAsArrayBuffer(new Blob([chunk.file]))
        reader.onload = data => {
          spark.append(data.target.result)
          chunk.hash = spark.end()
          resolve()
        }
      })
    },
    //给每个chunk添加hash和name
    calculateChunkHash () {
      return new Promise((resolve, reject) => {
        const spark = new Spark.ArrayBuffer()
        const requests = this.chunks.map((chunk, index) => {
          return this.pullHander(spark, chunk, index)
        })
        Promise.all(requests).then(() => {
          resolve()
        })
      })
    },
    async upload () {
      if (!this.fileData) {
        this.$message.warning('请选择文件')
        return 
      }
      //根据图片头信息来判断上传的是否为图片
      // if (!await this.isImage(this.fileData)) {
      //   return this.$message.error('文件格式错误')
      // }
      // =============== 分片上传 ================================
      const chunks = this.createChunk(this.fileData)
      //生成哈希
      const hash = await this.calculateHashByWebWorker(chunks)
      // const hash1 = await this.calculateHashByRequestIdleCallback(chunks)
      // const hash2 = await this.calulateSamplingHash(chunks)
      // console.log(hash, hash1, hash2, 'hash')
      // await this.calculateChunkHash()
      //检查文件是否已上传
      const fileExt = this.fileData.name.split('.').pop()
      // uploaded：文件是否已上传，uploadedList：上传的分片列表
      const { data: { uploaded, uploadedList } } = await this.$axios.get('/checkFile', {
        params: {
          hash,
          ext: fileExt
        }
      })
      if (uploaded) {
        this.$message.success('秒传成功')
        return
      }
      //断点续传  根据之前上传的文件
      this.chunks = chunks.map((chunk, index) => {
        const fileName = `${hash}-${index}`
        return {
          file: new File([chunk.file], fileName + '.' + fileExt, { type: 'image/mp4' }),
          name: fileName,
          hash,
          progress: uploadedList.includes(fileName) ? 100 : 0 //如果当前分片已经上传，进度直接设置为100
        }
      })
      this.uploadRequest(hash)
    },
    handlerChange (e) {
      const [file] = e.target.files
      if (!file) return
      this.fileData = file
    },
    //获取用户信息
    async getUserInfo () {
      await this.$axios.get('/user/getUserInfo')
    }
  }
}
</script>

<style lang="less">
html, body {
  background: #dddddd;
}
ul, li, ol {
  list-style: none;
}
.drag {
  width: 500px;
  height: 100px;
  text-align: center;
  line-height: 100px;
  border: 1px dashed #41B883;
  background-color: #fff;
  .file {
    // visibility: hidden;
  }
}
.grid {
  .grid-block {
    width: 24px;
    height: 24px;
    line-height: 24px;
    border: 1px solid #000;
    background-color: #eee;
    float: left;
    .success {
      background-color: greenyellow;
    }
    .error {
      background-color: red;
    }
  }
}
</style>
